import { GlSprintf, GlLink, GlBadge } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { REPORT_TYPES_TO_HUMAN_READABLE } from 'ee/security_dashboard/components/shared/vulnerability_details_graphql/constants';
import Details from 'ee/security_dashboard/components/shared/vulnerability_details_graphql/index.vue';
import DetailsSection from 'ee/security_dashboard/components/shared/vulnerability_details_graphql/details_section.vue';
import DetailsSectionListItem from 'ee/security_dashboard/components/shared/vulnerability_details_graphql/details_section_list_item.vue';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import GenericReportSection from 'ee/vulnerabilities/components/generic_report/report_section_graphql.vue';
import { vulnerabilityDetails } from 'ee_jest/security_dashboard/components/pipeline/mock_data';
import { getHttpString } from 'ee/vue_shared/security_reports/components/helpers';

const TEST_VULNERABILITY = {
  descriptionHtml: 'Description in <strong>HTML</strong>',
  description: 'Description in <strong>plain text</strong>',
  severity: 'HIGH',
  falsePositive: false,
  project: {
    name: 'Gitlab.com',
    webUrl: 'http://gitlab.com',
  },
  details: Object.values(vulnerabilityDetails),
  evidence: null,
};

describe('ee/security_dashboard/components/shared/vulnerability_details_graphql/index.vue', () => {
  let wrapper;

  const createComponent = (options = {}) => {
    wrapper = shallowMountExtended(Details, {
      propsData: {
        ...TEST_VULNERABILITY,
      },
      stubs: {
        GlSprintf,
        DetailsSection,
        DetailsSectionListItem,
      },
      ...options,
    });
  };

  const expectToBeDetailsSection = (sectionWrapper, { heading = '' } = {}) => {
    expect(sectionWrapper.is(DetailsSection)).toBe(true);
    expect(sectionWrapper.props('heading')).toBe(heading);
  };

  const expectOptionalFieldsNotRendering = (optionalFields = []) => {
    it.each(optionalFields)('does not render the optional field: "%s"', (testId) => {
      expect(wrapper.findByTestId(testId).exists()).toBe(false);
    });
  };

  describe('false positive alert', () => {
    const findFalsePositiveAlert = () => wrapper.findComponent(FalsePositiveAlert);

    it('shows the false positive alert', () => {
      createComponent({
        propsData: {
          ...TEST_VULNERABILITY,
          falsePositive: true,
        },
      });

      expect(findFalsePositiveAlert().exists()).toBe(true);
    });

    it('does not show the false positive alert', () => {
      createComponent();

      expect(findFalsePositiveAlert().exists()).toBe(false);
    });
  });

  describe('description section', () => {
    beforeEach(createComponent);

    const findDescriptionSection = () => wrapper.findByTestId('description-section');

    it('is a details section with the correct heading', () => {
      expectToBeDetailsSection(findDescriptionSection(), { heading: 'Description' });
    });

    it(`contains the vulnerability's HTML description`, () => {
      expect(findDescriptionSection().html()).toContain(TEST_VULNERABILITY.descriptionHtml);
    });

    it(`contains the vulnerability's plaintext description when descriptionHtml is not available`, () => {
      createComponent({
        propsData: {
          ...TEST_VULNERABILITY,
          descriptionHtml: '',
        },
      });
      expect(findDescriptionSection().text()).toContain(TEST_VULNERABILITY.description);
    });
  });

  describe('main section', () => {
    it('is a details section', () => {
      createComponent();
      expectToBeDetailsSection(wrapper.findByTestId('main-section'));
    });

    describe('required fields', () => {
      beforeEach(createComponent);

      expectOptionalFieldsNotRendering([
        'report-type-list-item',
        'state-list-item',
        'scanner-list-item',
      ]);

      it('renders the severity with a badge', () => {
        const severity = wrapper.findByTestId('severity-list-item');

        expect(severity.text()).toContain('Severity:');
        expect(severity.findComponent(SeverityBadge).exists()).toBe(true);
      });

      it('renders the project with a link to it', () => {
        const project = wrapper.findByTestId('project-list-item');

        expect(project.text()).toContain('Project:');
        expect(project.findComponent(GlLink).attributes('href')).toBe(
          TEST_VULNERABILITY.project.webUrl,
        );
      });
    });

    describe('optional fields', () => {
      it('renders the state with a badge', () => {
        const TEST_STATUS = 'DETECTED';
        createComponent({
          propsData: {
            ...TEST_VULNERABILITY,
            state: TEST_STATUS,
          },
        });

        const status = wrapper.findByTestId('state-list-item');

        expect(status.text()).toContain('Status:');
        expect(status.findComponent(GlBadge).text()).toBe(TEST_STATUS);
      });

      it.each(Object.entries(REPORT_TYPES_TO_HUMAN_READABLE))(
        'renders the report type: %s',
        (reportType, humanReadableReportType) => {
          createComponent({
            propsData: {
              ...TEST_VULNERABILITY,
              reportType,
            },
          });

          expect(wrapper.findByTestId('report-type-list-item').text()).toContain(
            humanReadableReportType,
          );
        },
      );

      describe('renders information about the used scanner', () => {
        const TEST_SCANNER_NAME = 'ESLint';
        const TEST_SCANNER_VERSION = '8.26.0';

        it.each`
          description                            | scanner                                                       | expectedText
          ${'a scanner with a name'}             | ${{ name: TEST_SCANNER_NAME }}                                | ${`Scanner: ${TEST_SCANNER_NAME}`}
          ${'a scanner with a name and version'} | ${{ name: TEST_SCANNER_NAME, version: TEST_SCANNER_VERSION }} | ${`Scanner: ${TEST_SCANNER_NAME} (version ${TEST_SCANNER_VERSION})`}
        `('renders $description', ({ scanner, expectedText }) => {
          createComponent({
            propsData: {
              ...TEST_VULNERABILITY,
              scanner,
            },
          });

          expect(wrapper.findByTestId('scanner-list-item').text()).toBe(expectedText);
        });
      });

      describe('evidence', () => {
        const findEvidenceListItem = () => wrapper.findByTestId('evidence-list-item');

        it('is rendered', () => {
          const summary = 'Invalid status codes indicate an error.';

          createComponent({
            propsData: {
              ...TEST_VULNERABILITY,
              evidence: { summary },
            },
          });
          expect(findEvidenceListItem().text()).toContain(summary);
          expect(findEvidenceListItem().props('label')).toBe('Evidence:');
        });

        it('is not rendered', () => {
          createComponent();

          expect(findEvidenceListItem().exists()).toBe(false);
        });
      });
    });
  });

  describe('request/response section', () => {
    const findRequestResponseSection = () => wrapper.findByTestId('request-response-section');
    const findRequestItem = () => wrapper.findByTestId('request-item');
    const findResponseItem = () => wrapper.findByTestId('response-item');
    const findRecordedResponseItem = () => wrapper.findByTestId('recorded-response-item');
    const request = {
      url: 'http://example.com/requestUrl',
      body: 'request body',
      method: 'request method',
      headers: [
        { name: 'headers name - 1', value: 'headers value - 1' },
        { name: 'headers name - 2', value: 'headers value - 2' },
      ],
    };

    const response = {
      body: 'response body',
      statusCode: '200',
      reasonPhrase: 'response reasonPhrase',
      headers: [
        { name: 'response headers name - 1', value: 'response headers value - 1' },
        { name: 'response headers name - 2', value: 'response headers value - 2' },
      ],
    };

    const supportingMessages = [
      {
        name: 'Recorded',
        response,
      },
    ];

    describe.each`
      type                    | evidence              | data                              | component
      ${'request'}            | ${request}            | ${request}                        | ${findRequestItem}
      ${'response'}           | ${response}           | ${response}                       | ${findResponseItem}
      ${'supportingMessages'} | ${supportingMessages} | ${supportingMessages[0].response} | ${findRecordedResponseItem}
    `('with $type information', ({ type, evidence, data, component }) => {
      beforeEach(() => {
        createComponent({
          propsData: {
            ...TEST_VULNERABILITY,
            evidence: {
              [type]: evidence,
            },
          },
        });
      });

      it('renders request/response section', () => {
        expectToBeDetailsSection(findRequestResponseSection(), { heading: 'Request/Response' });
      });

      it(`renders the ${type} code block section`, () => {
        expect(component().exists()).toBe(true);
        expect(component().findComponent(CodeBlock).props('code')).toBe(getHttpString(data));
      });
    });

    describe('without information', () => {
      it('does not get rendered', () => {
        createComponent();

        expect(findRequestItem().exists()).toBe(false);
        expect(findResponseItem().exists()).toBe(false);
        expect(findRecordedResponseItem().exists()).toBe(false);
      });
    });
  });

  describe('additional info section', () => {
    const findAdditionalInfoSection = () => wrapper.findByTestId('additional-info-section');
    const findAssertItem = () => wrapper.findByTestId('assert-item');

    it('is rendered with the evidence source name', () => {
      const sourceName = 'some assertion';

      createComponent({
        propsData: {
          ...TEST_VULNERABILITY,
          evidence: { source: { name: sourceName } },
        },
      });

      expectToBeDetailsSection(findAdditionalInfoSection(), { heading: 'Additional Info' });
      expect(findAssertItem().text()).toContain(sourceName);
      expect(findAssertItem().props('label')).toBe('Assert:');
    });

    it('is not rendered', () => {
      createComponent();

      expect(findAdditionalInfoSection().exists()).toBe(false);
    });
  });

  describe('location section', () => {
    const findLocationSection = () => wrapper.findByTestId('location-section');

    describe('with no location data', () => {
      beforeEach(() =>
        createComponent({
          propsData: {
            ...TEST_VULNERABILITY,
            location: {},
          },
        }),
      );

      it('does not get rendered', () => {
        expect(findLocationSection().exists()).toBe(false);
      });
    });

    describe('with location data', () => {
      const findFileLocation = () => wrapper.findByTestId('location-file-list-item');

      beforeEach(() =>
        createComponent({
          propsData: {
            ...TEST_VULNERABILITY,
            location: {
              file: 'index.js',
            },
          },
        }),
      );

      it('is a details section with the correct heading', () => {
        expectToBeDetailsSection(findLocationSection(), { heading: 'Location' });
      });

      it('renders the file name as text', () => {
        expect(findLocationSection().text()).toContain('index.js');
        expect(findFileLocation().findComponent(GlLink).exists()).toBe(false);
      });

      describe('with a blob path', () => {
        it('does not append a line range when line numbers are not present', () => {
          createComponent({
            propsData: {
              ...TEST_VULNERABILITY,
              location: {
                file: 'index.js',
                blobPath: 'index.js',
              },
            },
          });

          expect(trimText(findFileLocation().text())).toBe('File: index.js');
        });

        it.each`
          description                                                 | lineData                            | expectedLineRange
          ${'end line is after start line - using numbers'}           | ${{ startLine: 0, endLine: 1 }}     | ${'0-1'}
          ${'end line is after start line - using strings'}           | ${{ startLine: '0', endLine: '1' }} | ${'0-1'}
          ${'end line is equal to start line - using numbers'}        | ${{ startLine: 1, endLine: 1 }}     | ${'1'}
          ${'end line is equal to start line - using strings'}        | ${{ startLine: '1', endLine: '1' }} | ${'1'}
          ${'end line is greater than to start line - using strings'} | ${{ startLine: '1', endLine: '0' }} | ${'1'}
          ${'end line is greater than to start line - using numbers'} | ${{ startLine: 1, endLine: 0 }}     | ${'1'}
        `(
          `links to the vulnerable file's line range "$expectedLineRange" when $description`,
          ({ lineData, expectedLineRange }) => {
            const location = {
              blobPath: '/project/namespace/-/blob/e3343434/src/js/main.js',
              file: '/src/js/main.js',
              ...lineData,
            };
            createComponent({
              propsData: {
                ...TEST_VULNERABILITY,
                location,
              },
            });

            const { blobPath, file } = location;
            const fileLink = findFileLocation().findComponent(GlLink);

            expect(fileLink.attributes('href')).toBe(`${blobPath}#L${expectedLineRange}`);
            expect(fileLink.text()).toBe(`${file}:${expectedLineRange}`);
          },
        );
      });
    });
  });

  describe('links section', () => {
    const findLinksSection = () => wrapper.findByTestId('links-section');
    const findAllLinks = () => findLinksSection().findAllComponents(GlLink);

    describe('with no links', () => {
      beforeEach(createComponent);

      it('does not get rendered', () => {
        expect(findLinksSection().exists()).toBe(false);
      });
    });

    describe('with links', () => {
      const TEST_LINK_WITH_NAME = {
        url: 'http://link-to-vulnerability-1',
        name: 'link to vulnerability',
      };
      const TEST_LINK_WITHOUT_NAME = { url: 'http://link-to-vulnerability-2' };
      const TEST_LINKS = [TEST_LINK_WITH_NAME, TEST_LINK_WITHOUT_NAME];

      beforeEach(() =>
        createComponent({
          propsData: {
            ...TEST_VULNERABILITY,
            links: TEST_LINKS,
          },
        }),
      );

      it('is a details section with the correct heading', () => {
        expectToBeDetailsSection(findLinksSection(), { heading: 'Links' });
      });

      it('renders a list of links', () => {
        expect(findAllLinks()).toHaveLength(TEST_LINKS.length);
      });

      it.each`
        linkType                       | link                      | expectedLinkText
        ${'with a "name" property'}    | ${TEST_LINK_WITH_NAME}    | ${TEST_LINK_WITH_NAME.name}
        ${'without a "name" property'} | ${TEST_LINK_WITHOUT_NAME} | ${TEST_LINK_WITHOUT_NAME.url}
      `('renders a link $linkType correctly', ({ link, expectedLinkText }) => {
        expect(findAllLinks().at(TEST_LINKS.indexOf(link)).text()).toBe(expectedLinkText);
      });
    });
  });

  describe('identifiers section', () => {
    const findIdentifiersSection = () => wrapper.findByTestId('identifiers-section');
    const findIdentifierItems = () => findIdentifiersSection().findAll('li');

    describe('with no identifiers', () => {
      beforeEach(createComponent);

      it('does not get rendered', () => {
        expect(findIdentifiersSection().exists()).toBe(false);
      });
    });

    describe('with identifiers', () => {
      const TEST_IDENTFIER_WITH_URL = {
        url: 'http://identifier-1',
        name: 'identifier 1',
      };
      const TEST_IDENTIFIER_WITHOUT_URL = { name: 'identifier 2' };
      const TEST_IDENTIFIERS = [TEST_IDENTFIER_WITH_URL, TEST_IDENTIFIER_WITHOUT_URL];

      beforeEach(() =>
        createComponent({
          propsData: {
            ...TEST_VULNERABILITY,
            identifiers: TEST_IDENTIFIERS,
          },
        }),
      );

      it('is a details section with the correct heading', () => {
        expectToBeDetailsSection(findIdentifiersSection(), { heading: 'Identifiers' });
      });

      it('renders a list of identifiers', () => {
        expect(findIdentifierItems()).toHaveLength(TEST_IDENTIFIERS.length);
      });

      it.each`
        identifierType                | identifier                     | shouldContainLink
        ${'with a "url" property'}    | ${TEST_IDENTFIER_WITH_URL}     | ${true}
        ${'without a "url" property'} | ${TEST_IDENTIFIER_WITHOUT_URL} | ${false}
      `('renders an identifier $identifierType correctly', ({ identifier, shouldContainLink }) => {
        const identifierItem = findIdentifierItems().at(TEST_IDENTIFIERS.indexOf(identifier));
        const link = identifierItem.findComponent(GlLink);

        expect(identifierItem.text()).toBe(identifier.name);
        expect(link.exists()).toBe(shouldContainLink);

        if (shouldContainLink) {
          expect(link.attributes('href')).toBe(identifier.url);
        }
      });
    });
  });

  describe('assets section', () => {
    const findAssetsSection = () => wrapper.findByTestId('assets-section');
    const findAssetsItems = () => findAssetsSection().findAll('li');

    describe('with no assets', () => {
      beforeEach(createComponent);

      it('does not get rendered', () => {
        expect(findAssetsSection().exists()).toBe(false);
      });
    });

    describe('with assets', () => {
      const TEST_ASSET_WITH_URL = {
        name: 'asset 1',
        url: 'http://gitlab.com/asset-1',
      };
      const TEST_ASSET_WITHOUT_URL = {
        name: 'asset 2',
      };
      const TEST_ASSETS = [TEST_ASSET_WITH_URL, TEST_ASSET_WITHOUT_URL];

      beforeEach(() =>
        createComponent({
          propsData: {
            ...TEST_VULNERABILITY,
            assets: TEST_ASSETS,
          },
        }),
      );

      it('is a details section with the correct heading', () => {
        expectToBeDetailsSection(findAssetsSection(), { heading: 'Reproduction Assets' });
      });

      it('renders the asset items', () => {
        expect(findAssetsItems()).toHaveLength(TEST_ASSETS.length);
      });

      it('renders an asset that contains "url" as a link', () => {
        const assetItem = findAssetsItems().at(TEST_ASSETS.indexOf(TEST_ASSET_WITH_URL));
        const assetLink = assetItem.findComponent(GlLink);

        expect(assetLink.attributes('href')).toBe(TEST_ASSET_WITH_URL.url);
        expect(assetLink.text()).toBe(TEST_ASSET_WITH_URL.name);
      });

      it('renders an asset that only contains "name" as text', () => {
        const assetItem = findAssetsItems().at(TEST_ASSETS.indexOf(TEST_ASSET_WITHOUT_URL));

        expect(assetItem.findComponent(GlLink).exists()).toBe(false);
        expect(assetItem.text()).toBe(TEST_ASSET_WITHOUT_URL.name);
      });
    });
  });

  describe('generic report', () => {
    beforeEach(createComponent);

    it('should render and receive the correct props', () => {
      expect(wrapper.findComponent(GenericReportSection).props()).toMatchObject({
        reportItems: TEST_VULNERABILITY.details,
      });
    });
  });
});
